# -*- coding: utf-8 -*-

__authors__ = ["Sylvain Hellegouarch (sh@defuze.org)"]
__contributors__ = ['Tiago Cogumbreiro <cogumbreiro@users.sf.net>', 'Peter Russell']
__date__ = "2007/01/26"
__copyright__ = """
Copyright (c) 2005, 2006, 2007, Sylvain Hellegouarch
"""

__license__ = """
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, 
are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice, 
      this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice, 
      this list of conditions and the following disclaimer in the documentation 
      and/or other materials provided with the distribution.
    * Neither the name of Sylvain Hellegouarch nor the names of his contributors 
      may be used to endorse or promote products derived from this software 
      without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

"""
26/01/2007 - Added user_name attribute to request object (thanks Peter)
16/12/2006 - Added Peter Russell patch to support pre-hashed passwords
             so that now you can store hashed values instead of clear text
           - Should have also fixed the staticfilter case
10/04/2006 - Refactored the filter
30/12/2005 - Fixed to match lower cases attributes used by CherryPy
07/12/2005 - Unit test added
             Also added support for the max replay feature
06/12/2005 - Updated to match CherryPy 2.2.0-beta revision 862 (Sylvain)
"""

# This version has been test against CherryPy 2.2.2 RC2

#
# This is a simple filter for Digest and Basic Authorization schemes
# as described in RFC 2617
# http://www.ietf.org/rfc/rfc2617.txt
#

import time
import os.path
from stat import ST_MTIME
from os import stat

import cherrypy
from cherrypy.filters.basefilter import BaseFilter
import httpauth

class HttpAuthFilter(BaseFilter):
    """Class that must be called in your filters list.

    realm - see RFC 2617
    privateKey - seed for the nonce field of the DIGEST algorithm
    unauthorizedPath - a CherryPy exposed callable when we need to send the unauthorized message
    maxReplay - allows you to set how many times the same Digest authentification value can be used from a given user agent
        defaults to 0 which dismiss the replay checker 
    retrieveUsersFunc - a function that returns a dictionnary of
        user:password if storRealmsWithPasswords is False, or
        user:{realm:password} if not.  If storeHashedCredentials is
        True, then password will be the hash of username:realm:password.
        handy if you want to retrieve your users from a database for instance
    cppasswordPath - path of the file maintaining the user:password tuples.
        you can leave it to None when using retrieveUsersFunc
    """
    def __init__(self, realm, privateKey, unauthorizedPath,
                 maxReplay=0, retrieveUsersFunc=None,
                 cppasswordPath=None, storeRealmsWithPasswords=False,
                 storeHashedCredentials=False):
        self.cppasswordPath = cppasswordPath
        self.cppasswordLastModified = None
        self.users = {}
        self.realm = realm
        self.privateKey = privateKey
        self.maxReplay = maxReplay
        self.unauthorizedPath = unauthorizedPath
        self.retrieveUsersFunc = retrieveUsersFunc
        if retrieveUsersFunc is None:
            self.retrieveUsersFunc = self._loadAuthorizedUsers
        self.storeHashedCredentials = storeHashedCredentials
        if self.storeHashedCredentials == True:
            self.storeRealmsWithPasswords = True
        else:
            self.storeRealmsWithPasswords = storeRealmsWithPasswords

    def _loadAuthorizedUsers(self):
        # We don't to reload the file for each request. Let's reload it only when it is modified
        if not self.cppasswordLastModified or self.cppasswordLastModified != stat(self.cppasswordPath)[ST_MTIME]:
            self.cppasswordLastModified = stat(self.cppasswordPath)[ST_MTIME]
            cppassword = file(self.cppasswordPath, 'rb')
            for line in cppassword:
                if self.storeRealmsWithPasswords:
                    username, realm, password = line.split(':')
                    if username in self.users:
                        self.users[username][realm] = password
                    else:
                        self.users[username] = {realm:password.rstrip()}
                else:
                    username, password = line.split(':')
                    self.users[username] = password.rstrip()
            cppassword.close()
        return self.users

    def on_start_resource(self):
        # Make sure it will not be authorized whatever happens in between
        cherrypy.request.isAuthorized = False
        cherrypy.request.execute_main = False

    def before_request_body(self):
        # Check if we have an authorization request header
        if cherrypy.request.headerMap.has_key('Authorization'):
            # Parse the header
            ah = httpauth.parseAuthorization(cherrypy.request.headerMap['Authorization'])
            # someone might try to trick us with a request we can't understand
            # let's ditch the request
            if ah is None:
                raise cherrypy.HTTPError(400, 'Bad Request')
            else:
                # If we are dealing with hashed credentials, we need
                # the realm to be in the auth_map for basic authentication.
                if self.storeHashedCredentials and not 'realm' in ah:
                    ah['realm'] = self.realm

                # the nc token of the Digest authentification
                # allows a server to check for attack replay by setting a
                # limit to the nc value
                # If yo use the value to low, you'll force your users to
                # retype their credentials every self.maxReplay
                # setting a higher value could decrease the relevancy of this feature
                # read http://www.apps.ietf.org/rfc/rfc2617.html#sec-4.5
                # this is not really powerful though because it'll only work
                # on user agents telling the truth on that value...
                if self.maxReplay > 0:
                    nc = ah.get('nc', None)
                    if nc is not None:
                        if int(nc) > self.maxReplay:
                            # we force the user agent to ask the client his credential again
                            return
                    
                # Is the user authorized?
                password = self.retrieveUsersFunc().get(ah["username"], None)
                if self.storeRealmsWithPasswords and password:
                    password = password.get(self.realm, None)
                method = cherrypy.request.method.upper()
                if httpauth.checkResponse(ah, password,
                                          method=method,
                                          password_is_hashed=self.storeHashedCredentials):
                    # yeah the user is authorized
                    cherrypy.request.isAuthorized = True
                    cherrypy.request.user_name = ah["username"]
                    if method not in ["POST", "PUT"]:
                        cherrypy.request.processRequestBody = False

    def before_main(self):
        # this should make this filter to work fine with static content as well
        if cherrypy.config.get('static_filter.on', False) is False:
            cherrypy.request.execute_main = True
        if not cherrypy.request.isAuthorized:
            cherrypy.response.status = '401 Unauthorized'
            cherrypy.request.object_path = self.unauthorizedPath

    def on_end_resource(self):
        if not cherrypy.request.isAuthorized:
            cherrypy.response.header_list.append(('WWW-Authenticate', httpauth.digestAuth(self.realm)))
            cherrypy.response.header_list.append(('WWW-Authenticate', httpauth.basicAuth(self.realm)))
            
